<?php
/**
 * dbfront
 * (c) 2010-2013 Stephen Adkins <spadkins@gmail.com>
 * This software began development in 2010.
 * It is based on code dating from 1999.
 *
 * License: GPL 2.0
 * The contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL").
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 * */
namespace API;
class Auth extends \App\SessionObject {
    # private $dbh, $persistent;

    /**
     * connect () - returns a client_connection_token necessary for getUserAuthenticationSpecs(). Works only in test_mode. Should normally be found in FrontSuite.session.client_connection_token.
     * @param boolean success [OUT]
     * @param string client_connection_token [OUT]
     */
    public function connect ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;
        if (!$test_mode) throw new \Exception('ERROR: insufficient parameters supplied (0006)');
        $response['client_connection_token'] = $context->authentication()->getClientConnectionToken();
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    /**
     * user_auth_info (user_token, client_connection_token) - checks to see if a user record exists and returns a set of values if it does.
     * user_auth_info is done before login in order to retrieve the salt, hashing algorithm, and algorithm options needed to produce an auth_token.
     * @param string  client_connection_token [IN] (required) this token exists in a Javascript client's "FrontSuite.connection.client_connection_token" variable, but can also be retrieved for use by by non-Javascript clients with the 'connect' API call (this foils IP-spoofing attacks)
     * @param string  user_token [IN] (recommended) The user_token is generally used to identify a user. If so, an '@' indicates it is an email, all digits is a cell_phone, otherwise is a username.  One of 'user_token', 'email', 'username', or 'cell_phone' MUST be provided to identify the user.
     * @param string  email [IN] (optional) Alternatively, the email may be specifically used to identify the user. Also, when creating a user record, if the email is NOT provided, a temporary/fake one will be created. If the email is provided in the user_token, that will override this email field.
     * @param string  username [IN] (optional) Alternatively, the username may be specifically used to identify the user. Also, when creating a user record, if the username is NOT provided, a temporary/fake one will be created. If the username is provided in the user_token, that will override this username field.
     * @param string  cell_phone [IN] (optional) Alternatively, the cell_phone may be specifically used to identify the user.
     * @param boolean success [OUT] - without 'create', success is true if the user record exists and is active. with 'create', success is true if the user record is available for registration
     * @param integer user_id [OUT] unique ID for the (possibly new) user (e.g. 3)
     * @param string  user_salt [OUT] per-user cryptographically random 256-bit salt (e.g. dwfgTc.0FeoyYcJEs3A8zNh3XYjE6PL0A201yJbwL2LH8UG6tPdJ5dPj8irQvVSI)
     * @param string  algorithm [OUT] string indicating the algorithm to be used to produce the password hash (e.g. SHA256xN)
     * @param string  algorithm_option [OUT] string indicating any options used in the algorithm to produce the password hash (e.g. 16384)
     * @param string  message [OUT] whenever success is false, there will be a message to explain why
     */
    public function user_auth_info ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        if (isset($request['create'])) throw new \Exception ('create option not allowed on user_auth_info');
        if (isset($request['recaptcha_challenge_field'])) throw new \Exception ('recaptcha_challenge_field option not allowed on user_auth_info');
        if (isset($request['recaptcha_response_field'])) throw new \Exception ('recaptcha_response_field option not allowed on user_auth_info');
        $response['success'] = true;
        $context->authentication()->getUserAuthenticationSpecs($request, $response);
        if ($trace) trace_exit($response);
    }

    /**
     * reserve_user (client_connection_token, email, ...) - checks to see if a user record exists and returns a set of values if it does.
     * reserve_user is done with the 'create' option before register in order create a new user record, reserve it for an hour, and return the above-mentioned values.
     * A user record represents a user account prior to activation, while active, and after it has been locked/disabled.
     * A user record passes through the following states:
     * It is first created when a guest is exploring whether to register using a specific username.
     * Status = [U]nused   : a user has been checked on as though to login. the record was generated in order to have salt to report.
     * Status = [R]eserved : a user has expressed interest in the username, its availability was reported, and it is reserved for him for one hour
     * Status = [C]laimed  : a user has registered for the username, but has not yet confirmed via the email-ed claim_token
     * Status = [A]ctive   : a user has been fully registered and confirmed
     * Status = [X]-locked/disabled : a user account has been disabled/locked and cannot log in any more, effectively blocking all e-mail addresses attached to it.
     * @param string  client_connection_token [IN] (required) this token exists in a Javascript client's 'FrontSuite.connection.client_connection_token' variable, but can also be retrieved for use by non-Javascript clients with the 'connect' API call (this foils IP-spoofing attacks)
     * @param string  email [IN] (recommended) Alternatively, the email may be specifically used to identify the user. Also, when creating a user record, if the email is NOT provided, a temporary/fake one will be created. If the email is provided in the user_token, that will override this email field.
     * @param string  cell_phone [IN] (optional) Alternatively, the cell_phone may be specifically used to identify the user.
     * @param string  username [IN] (optional) when creating a user record, if the username is NOT provided, a temporary one will be created.
     * @param integer recaptcha_challenge_field [IN] (recommended) if recaptcha validates, a user record will be created with its salt. captchas are often required in production.
     * @param integer recaptcha_response_field [IN] (recommended) if recaptcha validates, a user record will be created with its salt. captchas are often required in production.
     * @param string  password [IN] (optional) (test_mode only) this non-secure way of setting up a user with a password is for testing mode only
     * @param boolean success [OUT] - without 'create', success is true if the user record exists and is active. with 'create', success is true if the user record is available for registration
     * @param integer user_id [OUT] unique ID for the (possibly new) user (e.g. 3)
     * @param string  user_salt [OUT] per-user cryptographically random 256-bit salt (e.g. dwfgTc.0FeoyYcJEs3A8zNh3XYjE6PL0A201yJbwL2LH8UG6tPdJ5dPj8irQvVSI)
     * @param string  algorithm [OUT] string indicating the algorithm to be used to produce the password hash (e.g. SHA256xN)
     * @param string  algorithm_option [OUT] string indicating any options used in the algorithm to produce the password hash (e.g. 16384)
     * @param string  message [OUT] whenever success is false, there will be a message to explain why
     */
    public function reserve_user ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $request['create'] = 1;
        $response['success'] = true;
        $context->authentication()->getUserAuthenticationSpecs($request, $response);
        if ($trace) trace_exit($response);
    }

    /**
     * delete_user () - checks to see if a user record exists and returns a set of values if it does.
     * deletes a user (only available in test mode)
     * @param string  username [IN] (required) the username of the user to delete
     * @param boolean success [OUT] -  success is true if the user record and all related information has been deleted
     * @param string  message [OUT] whenever success is false, there will be a message to explain why
     */
    public function delete_user ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;
        if (!$test_mode) throw new \Exception('delete_user() only works in test_mode');
        if (!isset($request['username'])) throw new \Exception('delete_user(): must supply a username (or "ALL"). (add delete=1 to delete or delete=ALL to delete ALL)');
        $username = $request['username'];
        $response['success'] = true;
        $admin = $context->sessionObject('administration', array('class' => 'App\\Administration'));
        $admin->deleteUser($username, $request, $response);
        if ($trace) trace_exit($response);
    }

    /**
     * login (user_token, auth_token, session_expire_dttm) - this routine returns the user data structure and sets the ff_session cookie. It these are not needed
     * @param string   user_token [IN] - the user-identifier (normally an email address). if it has an '@' in it, it is an email address. If it is all digits, it is a phone number. Otherwise it is a username.
     * @param string   auth_token [IN] - computed on the client as FrontSuite.makeToken(username)
     * @param integer  authdata [IN] - (optional) (boolean) if supplied, then the user data structure is returned on success
     * @param datetime session_expire_dttm [IN]
     * @param boolean  success [OUT]
     * @param object   user [OUT] on success, this is a data structure describing the logged in user, his groups, and his permissions (only if 'authdata' option used)
     * @param integer  code [OUT] on error, this is the error code
     * @param string   message [OUT] on error, this is the error message
     * @param cookie   ff_session [OUT] on success, this cookie contains the user_token, auth_token, and session_expire_dttm, else contains 'guest' credentials
     */
    public function login ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;

        $user_token          = isset($request['user_token'])          ? $request['user_token']          : '';
        $auth_token          = isset($request['auth_token'])          ? $request['auth_token']          : '';
        $session_expire_dttm = isset($request['session_expire_dttm']) ? $request['session_expire_dttm'] : '';
        $group_id            = isset($request['group_id'])            ? $request['group_id']            : null;
        $app_id              = isset($request['app_id'])              ? $request['app_id']              : null;

        if (!$user_token)          throw new \Exception('user_token Not Supplied',          ERROR_AUTH_NOUSERTOKEN);
        if (!$auth_token)          throw new \Exception('auth_token Not Supplied',          ERROR_AUTH_NOAUTHTOKEN);
        if (!$session_expire_dttm) throw new \Exception('session_expire_dttm Not Supplied', ERROR_AUTH_SESSIONEXPIRED);

        $context->login($user_token, $auth_token, $session_expire_dttm, $group_id, $app_id);
        $user = $context->getAuthenticatedUser();

        if ($user_token === $user['user_token']) {  # the auth_token was good. we have been verified to be the user_token we claimed to be.
            $response['success']    = true;
        }
        else {
            $response['success']    = false;
            if ($test_mode) {
                $response['code']     = $user['error_code'];
                $response['message']  = $user['error_message'];
            }
            else {
                $response['code']     = ERROR_AUTH_BAD_PASSWORD;
                $response['message']  = 'Incorrect User or Password';
            }
        }
        $response['user_token'] = $context->getAuthenticatedUserToken();
        $response['user']       = $context->getAuthenticatedUser();
        $response['groups']     = $context->getAuthorizedGroups();
        $response['apps']       = $context->getAuthorizedApps();
        $response['perms']      = $context->getAuthorizedPerms();
        $response['databases']  = $context->getAuthorizedDatabases();

        if ($trace) trace_exit($response);
    }

    /**
     * state () - returns the application state. This includes options (configuration settings) set on the server, connection parameters, and session values.
     * Within the session values is same 'authdata' user data structure returned by login(), but for the currently logged in user.
     * @param COOKIE   ff-session [IN] - the currently logged in user (as indicated by the ff-session cookie) is used as input
     * @param boolean  success [OUT]
     * @param boolean  logged_in [OUT] - true if the authenticated user_token is not 'guest'
     * @param object   options [OUT] - configuration options
     * @param object   connection [OUT] - connection values
     * @param string   connection.client_connection_token [OUT] - the client connection token required as input in many other API calls
     * @param object   session [OUT] - session values (whatever is in the ff-session cookie) (these values are before authentication of the user (validation of the auth_token))
     * @param string   session.user_token [OUT] - the email address, cell phone number, or username that the user is intended to be logged in with
     * @param string   session.auth_token [OUT] - the authentication token, a hash of the password, 
     * @param string   session.session_expire_dttm [OUT] - the date/time when the authentication token will no longer work to authenticate the user
     * @param integer  session.group_id [OUT] - the current group that the user is in
     * @param integer  session.app_id [OUT] - the current application that the user is using
     * @param object   session.authdata [OUT] - information about the logged in user, the groups he is in, the applications he can run, and the permissions he has
     * @param string   session.authdata.user_token [OUT] - the authenticated user_token (sometimes a user is not successfully authenticated and becomes 'guest')
     * @param object   session.authdata.user [OUT] - information about the authenticated user
     * @param integer  session.authdata.user.group_id [OUT] - the group_id that the authenticated user is in
     * @param integer  session.authdata.user.app_id [OUT] - the app_id that the authenticated user is using
     * @param object   session.authdata.groups [OUT] - the groups the user is in
     * @param object   session.authdata.apps [OUT] - the applications the user can run while in his current group
     * @param object   session.authdata.perms [OUT] - the permissions the user has in the current application
     */
    public function state ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();

        $response['success']    = true;
        $public_options         = array();
        foreach ($context->publicOptions() as $var) {
            $public_options[$var] = $options[$var];
        }

        $auth                   = $context->authentication();

        $session                = $context->getSession();

        $authdata = array();
        $user_token             = $context->getAuthenticatedUserToken();
        $authdata['user_token'] = $user_token;
        $authdata['user']       = $context->getAuthenticatedUser();
        $authdata['groups']     = $context->getAuthorizedGroups();
        $authdata['apps']       = $context->getAuthorizedApps();
        $authdata['perms']      = $context->getAuthorizedPerms();
        $authdata['databases']  = $context->getAuthorizedDatabases();

        $response['logged_in']  = ($user_token !== 'guest');
        $response['options']    = $public_options;
        $response['connection'] = array('client_connection_token' => $auth->getClientConnectionToken());
        $response['session']    = $session;
        $response['authdata']   = $authdata;

        if ($trace) trace_exit($response);
    }

    /**
     * logout() - end the login session
     * @param boolean success [OUT] - success is always true
     * @param COOKIE ff-session [OUT] - sets the ff-session cookie with
     */
    public function logout ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $logout_request = array(
            'user_token' => APP_GUEST_USERNAME,
            'auth_token' => APP_DUMMY_AUTH_TOKEN,
            'session_expire_dttm' => APP_END_OF_TIME
        );
        $this->login($logout_request, $response);
        if ($trace) trace_exit($response);
    }

    /**
     * register (email, x)
     * @param string   email - (required) email address
     * @param string   x - (required) encrypted password hash (not the same as the auth_token)
     * @param string   username - (optional) requested username
     * @param string   first_name - (optional) real first name
     * @param string   middle_name - (optional) real first name
     * @param string   last_name - (optional) real first name
     * @param string   nick_name - (optional) real first name
     * @param date     birth_date - (optional) for security and legal (age) compliance
     * @param string   cell_phone - (optional) for security and alternative messaging
     * @param string   address1 - (optional) address (for geolocation/mapping)
     * @param string   city - (optional) city (for geolocation/mapping)
     * @param string   state_code - (optional) state code (for geolocation/mapping)
     * @param string   postal_code - (optional) postal/zip code (for geolocation/mapping)
     * @param string   country_code - (optional) counry code (for geolocation/mapping)
     * @param string   company - (optional) company
     * @param string   company_type - (optional) type of company
     * @param string   company_position - (optional) position within the company
     */
    public function register ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $registration = $context->sessionObject('registration', array('class' => 'FrontSuite\\Registration'));
        $registration->ajaxRegister($request, $response);
        if ($trace) trace_exit($response);
    }

    /**
     * modifySession($request, &$response)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function modifySession ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $safe_request = array();
        foreach (array('login_action', 'login_arg') as $key) {
            if (isset($request[$key]) && strlen($request[$key]) <= 255) {
                $safe_request[$key] = $request[$key];
            }
        }
        $context->modifySession($safe_request);
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }
    public function groups ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $response['user_token'] = $context->getAuthenticatedUserToken();
        $response['user_id']    = $context->getAuthenticatedUserId();
        $response['group_id']   = $context->getAuthenticatedGroupId();
        $response['groups']     = $context->getAuthorizedGroups();
        if ($trace) trace_exit($response);
    }
    public function apps ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $response['user_token'] = $context->getAuthenticatedUserToken();
        $response['user_id']    = $context->getAuthenticatedUserId();
        $response['group_id']   = $context->getAuthenticatedGroupId();
        $response['app_id']     = $context->getAuthenticatedAppId();
        $response['apps']       = $context->getAuthorizedApps();
        if ($trace) trace_exit($response);
    }
    public function perms ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $response['user_token'] = $context->getAuthenticatedUserToken();
        $response['user_id']    = $context->getAuthenticatedUserId();
        $response['group_id']   = $context->getAuthenticatedGroupId();
        $response['app_id']     = $context->getAuthenticatedAppId();
        $response['perms']      = $context->getAuthorizedPerms();
        $response['databases']  = $context->getAuthorizedDatabases();
        if ($trace) trace_exit($response);
    }
    public function prepare_password_reset ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $registration = $context->sessionObject('registration', array('class' => 'FrontSuite\\Registration'));
        $registration->ajaxPreparePasswordReset($request, $response);
        if ($trace) trace_exit($response);
    }
    public function initiate_password_reset ($request, &$response) {
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $registration = $context->sessionObject('registration', array('class' => 'FrontSuite\\Registration'));
        $registration->ajaxInitiatePasswordReset($request, $response);
        if ($trace) trace_exit($response);
    }
    /**
     * createUser (username, password) - shortcut to creating test users. this is only allowed in test_mode (use register() in production).
     * @param string username [IN]
     * @param string client_connection_token [IN]
     * @param boolean success [OUT]
     */
    public function createUser ($request, &$response) {      # only works in test_mode !!!
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;
        if (!$test_mode) throw new \Exception('ERROR: insufficient parameters supplied (0007a)');
        $context->authentication()->createUser($request, $response);
        if ($trace) trace_exit($response);
    }
    /**
     * validateCaptcha (username, password) - test out the validation of captchas. (use recaptcha_auto_validate config option to always return true). this is only allowed in test_mode.
     * @param string recaptcha_challenge_field [IN]
     * @param string recaptcha_response_field [IN]
     * @param boolean success [OUT]
     */
    public function validateCaptcha ($request, &$response) {      # only works in test_mode !!!
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;
        if (!$test_mode) throw new \Exception('ERROR: insufficient parameters supplied (0007)');
        $context->authentication()->validateCaptcha($request, $response);
        if ($trace) trace_exit($response);
    }
    public function makeToken ($request, &$response) {   # only works in test_mode !!!
        global $context, $options, $trace;
        if ($trace) trace_entry();
        $test_mode = (isset($options['test_mode']) && $options['test_mode']) ? $options['test_mode'] : 0;
        if (!$test_mode) throw new \Exception('ERROR: insufficient parameters supplied (0008)');
        $password = isset($request['password']) ? $request['password'] : "";
        $session_expire_dttm = isset($request['session_expire_dttm']) ? $request['session_expire_dttm'] : date('Y-m-d\TH:i:s\Z', time()+3600);
        $authentication = $context->authentication();
        $authdb = $context->authdb();
        list($algorithm, $algorithm_option, $salt) = $authdb->get_row('select algorithm, algorithm_option, salt from auth_user where username = ?', $username);
        $password_hash = $authentication->createPasswordHash($algorithm, $algorithm_option, $password, $salt);
        $auth_token = $authentication->createAuthToken($algorithm, $algorithm_option, $password_hash, $session_expire_dttm);
        $response['session_expire_dttm'] = $session_expire_dttm;
        $response['auth_token']          = $auth_token;
        $response['password_hash']       = $password_hash;
        $response['algorithm']           = $algorithm;
        $response['algorithm_option']    = $algorithm_option;
        if ($trace) trace_exit($response);
    }
}

